import logging
import sys
import os
import signal
import socket
import subprocess
import threading

import io.ioloop
import util.common

import channel

#------------------------------------------------------------------------------
# module globals
log = logging.getLogger('method')
method_descriptions = {}

#------------------------------------------------------------------------------
def parse_method(name, desc, tx):
    """Returns the Method object that is described by desc"""
    type = desc[0]
    if type == 'tcp_in':
        _, addr, port = desc
        return TcpIn(tx, name, (addr, int(port)))
    elif type == 'tcp_out':
        _, addr, port, interval = desc
        return TcpOut(tx, name, (addr, int(port)), float(interval))
    elif type == 'avahi':
        _, pub, browse, interval, tcp_in = desc
        return AvahiTcp(tx,
                        name, pub, browse, float(interval),
                        tx.get_method(tcp_in))
    elif type == 'fs':
        _, metafile, interval = desc
        return Fs(tx, name, metafile, float(interval))
    elif type == 'sms':
        _, sockfile, phbook = desc
        return Sms(tx, name, sockfile, phbook)
    
#------------------------------------------------------------------------------
def add_method_description(type, desc):
    """Adds a description to the list of implemented methods"""
    method_descriptions[type] = desc
    
#------------------------------------------------------------------------------
class Method:
    """Communications method that will be used to reach our peers.

    Abstract base class.
    """

    def __init__(self, name, tx):
        self.name_ = name
        self.tx_ = tx

    def name(self):
        return self.name_
    
    def start(self):
        """Start the method (register alarms, bind sockets)"""
        raise NotImplementedError

#------------------------------------------------------------------------------
class TcpIn(Method):
    """Communications method that will open a local server socket for
    remote hosts to connect to."""
    
    def __init__(self, tx, name, ap):
        """ap is the address,port of the local server socket to open"""
        Method.__init__(self, name, tx)
        self.ap_ = ap

    def __repr__(self):
        return '<method.TcpIn %s listening at %s:%s>' % \
               (self.name_, self.ap_[0], self.ap_[1])

    def start(self):
        log.info('Starting method %s...' % self)
        s = io.ioloop.ListenHandle(self.ap_,
                                   channel.Tcp.ConnectFactory(self.tx_, self.name()))
        self.server_sock_ = s
        io.ioloop.master_loop.add_handle(self.server_sock_)

add_method_description('tcp_in',
"""    tcp_in,<addr>,<port>

        TCP server socket. Creates a listening socket on addr:port.
""")

#------------------------------------------------------------------------------
class TcpOut(Method):
    """Communications method that will attempt to connect to a remote
    TCP/IP addressable host."""
    
    def __init__(self, tx, name, ap, interval):
        """ap is the address, port of the remote host
        interval is the delay in seconds between connection attempts"""
        Method.__init__(self, name, tx)
        self.ap_ = ap
        self.interval_ = interval

    def __repr__(self):
        return '<method.TcpOut %s connect to %s:%s, %d sec>' % \
               (self.name_, self.ap_[0], self.ap_[1], self.interval_)

    def start(self):
        log.info('Starting method %s...' % self)
        io.ioloop.master_loop.set_alarm(
            io.ioloop.Alarm(self.interval_, self.try_connection))

    def try_connection(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # XXX/bowei -- check to see if we need to connect
        try:
            log.debug('%s: trying connection' % self)
            sock.connect(self.ap_)
        except socket.error, e:
            log.debug('Connect failed: %s' % e)
            io.ioloop.master_loop.set_alarm(
                io.ioloop.Alarm(self.interval_, self.try_connection))
            return
        
        # this will register the channel with io.ioloop. since we haven't
        # gotten the handshake from the peer, we can't register this
        # with a peer yet.
        log.debug('%s: connect succeeded' % self)
        c = channel.Tcp(tx            = self.tx_,
                        sock          = sock,
                        # XXX/bowei -- get the actual interface
                        iface_id      = self.name(),
                        send_id_first = True,
                        close_hook    = self.restart_connect)
        io.ioloop.master_loop.add_handle(c)

    def restart_connect(self):
        """Give the system a bit of time to quiesce before retrying"""
        io.ioloop.master_loop.set_alarm(
            io.ioloop.Alarm(0.5, self.try_connection))
        
add_method_description('tcp_out',
"""    tcp_out,<addr>,<port>,<interval>

        TCP connection. Tries to connect to addr:port every interval sec.
""")

#------------------------------------------------------------------------------
class AvahiTcp(TcpIn):
    """Use the Avahi (Zero-conf) command line utilities to find peers

    Note: This needs to be paired with a tcp_in.
    """

    SERVICE_NAME = '_neon_tx._tcp'
    
    def __init__(self, tx, name, avahi_pub, avahi_browse, interval, tcp_in):
        assert isinstance(tcp_in, TcpIn)
        Method.__init__(self, name, tx)

        if avahi_pub == '-':
            avahi_pub = '/usr/bin/avahi-publish'
        if avahi_browse == '-':
            avahi_browse = '/usr/bin/avahi-browse'

        if ((not os.path.exists(avahi_pub)) or
            (not os.path.exists(avahi_browse))):
            raise Exception("Avahi configured, but can't find both avahi-publish (%s) and avahi-browse (%s)" % (avahi_pub, avahi_browse))
        
        self.avahi_pub_    = avahi_pub
        self.avahi_browse_ = avahi_browse
        self.interval_     = interval
        self.service_name_ = tx.neon_id()
        self.service_type_ = AvahiTcp.SERVICE_NAME
        self.tcp_in_       = tcp_in

        # Available local hosts and lock for the browse_thread
        self.available_    = {}
        self.lock_         = threading.Lock()
        self.quit_         = False
        # XXX/bowei - Very lame implementation, but what can you
        # really do?

    def __repr__(self):
        return '<method.AvahiTcp %s %s:%s>' % \
               (self.name_, self.service_name_, self.service_type_)

    def __del__(self):
        self.quit_ = True
        os.kill(self.pub_proc_.pid, signal.SIGTERM)
        
    def start(self):
        log.info('Starting method %s' % self)
        self.pub_proc_ = subprocess.Popen(
            [self.avahi_pub_, '-s',
             self.service_name_, self.service_type_,
             str(self.tcp_in_.ap_[1])],
            stdout = subprocess.PIPE,
            stderr = subprocess.STDOUT)
        self.browse_thread_ = threading.Thread(target = self.browse_thread)
        self.browse_thread_.start()
        io.ioloop.master_loop.set_alarm(
            io.ioloop.Alarm(self.interval_, self.try_connection))
        
    def try_connection(self):
        self.lock_.acquire()
        try:
            already_connected = [i[1] for i in self.tx_.get_channel_summary()
                                 if i[0] == 'tcp']
            aps = [self.available_[id]
                   for id in self.available_.keys()
                   if ((id < self.tx_.neon_id()) and
                       (self.available_[id] not in already_connected))]
        finally:
            self.lock_.release()

        if len(aps) > 0:
            log.debug('Available Avahi hosts: %s' % aps)
        for ap in aps:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            try:
                log.debug('%s: trying connection' % self)
                sock.connect(ap)
                log.debug('%s: connect succeeded' % self)
                c = channel.Tcp(tx            = self.tx_,
                                sock          = sock,
                                # XXX/bowei -- get the actual interface
                                iface_id      = self.name(),
                                send_id_first = True)
                io.ioloop.master_loop.add_handle(c)
            except socket.error, e:
                log.debug('Connect failed: %s' % e)
        io.ioloop.master_loop.set_alarm(
            io.ioloop.Alarm(self.interval_, self.try_connection))

    def browse_thread(self):
        """Thread calls avahi_browse to try to connect to other peers
        on the local network. Note: Only will try to connect from the
        higher id's to the lower id's to avoid parallel
        connections."""
        try:
            while not self.quit_:
                browse_proc = subprocess.Popen([self.avahi_browse_,
                                                '-t', '-r', '-p',
                                                AvahiTcp.SERVICE_NAME],
                                               stdout = subprocess.PIPE)
                response = [l.split(';')
                            for l in browse_proc.stdout.read().split()
                            if l[0] == '=']
                for r in response:
                    id = r[3]
                    ip = r[7]
                    port = int(r[8])
                    self.lock_.acquire()
                    self.available_[id] = (ip, port)
                    self.lock_.release()
                util.common.sleep_sec(0.5)
        except:
            return
            
add_method_description('avahi',
"""    avahi,<pub>,<browse>,<interval>,<tcp_in>

        Use avahi Zero-conf to automatically detect local Neon
        instances.  pub, browse is the location of the avahi-publish
        and avahi-browse programs respectively ('-' uses the default
        locations). interval is the number of seconds to wait between
        polling. tcp_in is the name of tcp daemon to advertise.
""")

#------------------------------------------------------------------------------
class Fs(Method):
    """Communications method that will be used for transfers done over
    the filesystem."""

    def __init__(self, tx, name, metafile, interval):
        """metafile is the location of the metafile to poll for
        interval is the polling interval"""
        Method.__init__(self, name, tx)
        self.metafile_ = metafile
        self.interval_ = interval

    def __repr__(self):
        return '<method.Fs %s metafile %s, %d sec>' % \
               (self.name_, self.metafile_, self.interval_)

    def start(self):
        log.info('Starting method %s...' % self)
        raise NotImplementedError()

add_method_description('fs',
"""    fs,<metafile>,<interval>

        Filesystem. Polls for metafile for sneakernet communication
        every interval seconds.
        """)

#------------------------------------------------------------------------------
class Sms(Method):
    """SMS communications method"""
    def __init__(self, tx, name, sockfile, phonebook_file):
        Method.__init__(self, name, tx)

        self.sockfile_  = sockfile
        # Note: phonebook can contain multiple entries for an alias
        self.phonebook_file_ = phonebook_file
        self.phonebook_ = []

        if not os.path.exists(sockfile): 
            log.error("SMS method: can't find sockfile: %s" % sockfile)
            sys.exit(1)
        if not os.path.exists(phonebook_file):
            log.error("SMS method: can't find phonebook: %s" % phonebook_file)
            sys.exit(1)

    def read_phonebook(self):
        for l in open(self.phonebook_file_, 'r'):
            l = l.strip()
            if l[0] == '#':
                continue
            alias, num = l.strip().split()
            log.debug('sms phonebook: %s -> %s' % (alias, num))
            self.phonebook_.append((alias,num))
        log.debug('sms phonebook loaded')
            
    def get_number(self, alias):
        return [n for a, n in self.phonebook_ if a == alias]

    def get_alias(self, num):
        for a, n in self.phonebook_:
            if n == num:
                return a
        return None

    def create_smsd_channel(self):
        log.debug('trying to connect to SMS daemon @ %s' % self.sockfile_)
        try:
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            sock.connect(self.sockfile_)
        except socket.error, e:
            log.error('connection to sms daemon failed: %s' % e)
            return
        c = channel.Sms(self.tx_, self.name(), self, sock)
        io.ioloop.master_loop.add_handle(c)

        for a, n in self.phonebook_:
            p = self.tx_.get_peer(a)
            pc = p.reg_comm(c)
            c.add_peer_comm(a, pc)
    
    def start(self):
        self.read_phonebook()
        self.create_smsd_channel()

add_method_description('sms',
"""    sms,<server socket>,<phone book>

        SMS transport. Server socket is the location of the sms daemon
        API socket. Phone book is a text file containing
        name-to-number aliases.
""")
